/**
 ** This file is part of FK Klistret Extensions. FK Klistret Extensions 
 ** is free software: you can redistribute it and/or modify it under the 
 ** terms of the GNU General Public License as published by the Free 
 ** Software Foundation, either version 3 of the License, or (at your option) 
 ** any later version.

 ** FK Klistret Extensions is distributed in the hope that it will be 
 ** useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
 ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 ** General Public License for more details. You should have received a
 ** copy of the GNU General Public License along with FK Klistret Extensions. 
 ** If not, see <http://www.gnu.org/licenses/>
 */
package se.fk.klistret.extension.service;

import java.util.Arrays;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import se.fk.klistret.extension.exceptions.ApplicationException;

import com.klistret.cmdb.ci.commons.Property;
import com.klistret.cmdb.ci.element.component.Publication;
import com.klistret.cmdb.ci.element.component.Software;
import com.klistret.cmdb.ci.element.context.PublicationType;
import com.klistret.cmdb.ci.pojo.Element;
import com.klistret.cmdb.ci.pojo.ElementType;
import com.klistret.cmdb.ci.pojo.Relation;
import com.klistret.cmdb.ci.pojo.RelationType;
import com.klistret.cmdb.ci.relation.Dependency;
import com.klistret.cmdb.service.ElementService;
import com.klistret.cmdb.service.ElementTypeService;
import com.klistret.cmdb.service.IdentificationService;
import com.klistret.cmdb.service.RelationService;
import com.klistret.cmdb.service.RelationTypeService;

/**
 * Abstract class extended by the Manifest, Konfiguration and Product services
 * to find composition relationships, versions (application software similar to
 * an Ivy module), and archives (application software similar to an Ivy
 * archive). It is important to point out that this class and it's extensions
 * are really specific to the Swedish Social Agency as a way to import
 * application software definitions into Klistret. A more general way would be
 * to import Ivy or Maven manifestations instead.
 * 
 * @author Matthew Young
 * 
 */
public abstract class ApplicationSoftwareHelper {
	private static final Logger logger = LoggerFactory
			.getLogger(ApplicationSoftwareHelper.class);

	/**
	 * Organizational context (dependency injection)
	 */
	protected String organization;

	/**
	 * Name pattern for archives (software similar to Ivy artifacts)
	 */
	protected static final Pattern archiveNamePattern = Pattern
			.compile("([A-Z0-9]+)_([0-9]{4}_[A-Z0-9]{3,4}+)_(([A-Z0-9]|-|_)+)(\\.|_)\\d{6}(_[A-Z0-9]{6}){0,1}\\.tar");

	/**
	 * Name pattern for version (software similar to Ivy modules)
	 */
	protected static final Pattern versionNamePattern = Pattern
			.compile("([A-Z0-9]{3,4})_([0-9]{4}_[A-Z0-9]{3,4}+)(_([A-Z0-9]+)){0,1}");

	/**
	 * Element service (dependency injection)
	 */
	protected ElementService elementService;

	/**
	 * Element type service (dependency injection)
	 */
	protected ElementTypeService elementTypeService;

	/**
	 * Relation service (dependency injection)
	 */
	protected RelationService relationService;

	/**
	 * Relation type service (dependency injection)
	 */
	protected RelationTypeService relationTypeService;

	/**
	 * Identification service
	 */
	protected IdentificationService identificationService;

	/**
	 * Software type
	 */
	protected ElementType softwareType;

	/**
	 * Publication type
	 */
	protected ElementType publicationType;

	/**
	 * Composition type
	 */
	protected RelationType dependencyType;

	/**
	 * Set organization
	 * 
	 * @param organization
	 */
	public void setOrganization(String organization) {
		this.organization = organization;
	}

	/**
	 * Set element service
	 * 
	 * @param elementService
	 */
	public void setElementService(ElementService elementService) {
		this.elementService = elementService;
	}

	/**
	 * Set element type service
	 * 
	 * @param elementTypeService
	 */
	public void setElementTypeService(ElementTypeService elementTypeService) {
		this.elementTypeService = elementTypeService;
	}

	/**
	 * Set relation service
	 * 
	 * @param relationService
	 */
	public void setRelationService(RelationService relationService) {
		this.relationService = relationService;
	}

	/**
	 * Set relation type service
	 * 
	 * @param relationTypeService
	 */
	public void setRelationTypeService(RelationTypeService relationTypeService) {
		this.relationTypeService = relationTypeService;
	}

	/**
	 * Set identification service
	 * 
	 * @param identificationService
	 */
	public void setIdentificationService(
			IdentificationService identificationService) {
		this.identificationService = identificationService;
	}

	/**
	 * Finds a unique dependency (relation) between a source and destination
	 * element.
	 * 
	 * @param source
	 * @param destination
	 * @return
	 */
	protected Integer hasDependency(Element source, Element destination) {
		String[] expressions = new String[] {
				"declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; /pojo:Relation[empty(pojo:toTimeStamp)][pojo:type/pojo:name eq \"{http://www.klistret.com/cmdb/ci/relation}Dependency\"]",
				String.format(
						"declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; /pojo:Relation/pojo:source[pojo:id eq %d]",
						source.getId()),
				String.format(
						"declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; /pojo:Relation/pojo:destination[pojo:id eq %d]",
						destination.getId()) };

		return relationService.count(Arrays.asList(expressions));
	}

	/**
	 * Finds a unique version (software) based on a label, not name
	 * 
	 * @param name
	 * @return
	 */
	protected Element getVersionByLabel(String label) {
		String[] expressions = new String[] { String
				.format("declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; declare namespace component=\"http://www.klistret.com/cmdb/ci/element/component\"; /pojo:Element[empty(pojo:toTimeStamp)][pojo:type/pojo:name eq \"{http://www.klistret.com/cmdb/ci/element/component}Software\"]/pojo:configuration[component:Label eq \"%s\"]",
						label) };

		return elementService.unique(Arrays.asList(expressions));
	}

	/**
	 * Finds a unique tar file (publication) by label (not name)
	 * 
	 * @param name
	 * @return
	 */
	protected Element getArchiveByLabel(String label) {
		String[] expressions = new String[] { String
				.format("declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; declare namespace component=\"http://www.klistret.com/cmdb/ci/element/component\"; /pojo:Element[empty(pojo:toTimeStamp)][pojo:type/pojo:name eq \"{http://www.klistret.com/cmdb/ci/element/component}Publication\"]/pojo:configuration[component:Label eq \"%s\"]",
						label) };

		return elementService.unique(Arrays.asList(expressions));
	}

	/**
	 * Finds a unique release (time-frame) by name
	 * 
	 * @param name
	 * @return
	 */
	protected Element getRelease(String name) {
		String[] expressions = new String[] { String
				.format("declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; /pojo:Element[pojo:name eq \"%s\" and empty(pojo:toTimeStamp)][pojo:type/pojo:name eq \"{http://www.klistret.com/cmdb/ci/element/context}Timeframe\"]",
						name) };

		return elementService.unique(Arrays.asList(expressions));
	}

	/**
	 * Gets software type (checks local cache first)
	 * 
	 * @return ElementType
	 */
	protected ElementType getSoftwareType() {
		if (softwareType == null)
			softwareType = elementTypeService
					.get("{http://www.klistret.com/cmdb/ci/element/component}Software");

		return softwareType;
	}

	/**
	 * Gets publication type (checks local cache first)
	 * 
	 * @return ElementType
	 */
	protected ElementType getPublicationType() {
		if (publicationType == null)
			publicationType = elementTypeService
					.get("{http://www.klistret.com/cmdb/ci/element/component}Publication");

		return publicationType;
	}

	/**
	 * Gets dependency type (checks local cache first)
	 * 
	 * @return RelationType
	 */
	protected RelationType getDependencyType() {
		if (dependencyType == null)
			dependencyType = relationTypeService
					.get("{http://www.klistret.com/cmdb/ci/relation}Dependency");

		return dependencyType;
	}

	/**
	 * Constructs a Dependency but does not create the relation.
	 * 
	 * @param source
	 * @param destination
	 * @return
	 */
	protected Relation doDependency(Element source, Element destination) {
		logger.debug(
				"Creating dependency between software {} and publication {}",
				source.getName(), destination.getName());

		Relation dependency = new Relation();
		dependency.setType(getDependencyType());
		dependency.setSource(source);
		dependency.setDestination(destination);
		dependency.setFromTimeStamp(new java.util.Date());
		dependency.setCreateTimeStamp(new java.util.Date());
		dependency.setUpdateTimeStamp(new java.util.Date());

		Dependency configuration = new Dependency();
		configuration.setName(String.format("%s,%s", source.getName(),
				destination.getName()));

		dependency.setConfiguration(configuration);
		return dependency;
	}

	/**
	 * Constructs an software but does not create the element.
	 * 
	 * @param label
	 * @return
	 */
	protected Element doSoftware(String label) {
		logger.debug("Creating software [label: {}]", label);

		String name = null;
		String version = null;
		String trailing = null;

		Matcher m = versionNamePattern.matcher(label);
		while (m.find()) {
			// Name (software context)
			name = m.group(1);

			// Version
			version = m.group(2);

			// Trailing
			if (m.group(3) != null) {
				trailing = m.group(3).substring(1, m.group(3).length());
			}
		}

		if (name == null)
			throw new ApplicationException(String.format(
					"Software label [%s] could not be parsed", label));

		Element element = new Element();
		element.setName(name == null ? label : name);
		element.setType(getSoftwareType());
		element.setFromTimeStamp(new Date());
		element.setCreateTimeStamp(new Date());
		element.setUpdateTimeStamp(new Date());

		Software configuration = new Software();
		configuration.setName(element.getName());

		// Organization
		if (!isOrganization(organization))
			throw new ApplicationException(String.format(
					"Organization [name: {}, label: {}] not registered",
					organization, label));
		else
			configuration.setOrganization(organization);

		// Software context
		if (!isSoftwareContext(name))
			throw new ApplicationException(String.format(
					"Software (context) [name: {}, label: {}] not registered",
					name, label));
		else {
			Property property = new Property();
			property.setName("software.context.ancestor");
			property.setValue(name);
			configuration.getProperty().add(property);
		}

		// Software version
		if (version == null)
			throw new ApplicationException(String.format(
					"Version [label: {}] not parsed", label));
		configuration.setVersion(version);

		// Trailing
		if (trailing != null) {
			Property property = new Property();
			property.setName("version.suffix");
			property.setValue(trailing);
			configuration.getProperty().add(property);
		}

		// Original label
		configuration.setLabel(label);

		element.setConfiguration(configuration);
		return element;
	}

	/**
	 * Constructs an publication (tar file) based on the label attribute but
	 * does not create the element.
	 * 
	 * @return
	 */
	protected Element doPublication(String label) {
		logger.debug("Creating publication [label: {}]", label);

		String softwareContext = null;
		String softwareContextOriginal = null;
		String version = null;
		String type = null;

		int prefixStart = 0;

		Matcher m = archiveNamePattern.matcher(label);
		while (m.find()) {
			softwareContext = m.group(1);

			version = m.group(2);

			type = m.group(3);

			prefixStart = m.start();
		}

		String prefixes = label.substring(0, prefixStart);

		/**
		 * Special case for KND with a type equal to P or F. The special
		 * place-holder is nulled to prevent unnecessary registration of special
		 * organization type prefixing. Later it was necessary to even swap out
		 * the original software context against a concatenation againt P or F.
		 */
		if (softwareContext != null && softwareContext.equals("KND")
				&& type != null && (type.endsWith("_P") || type.endsWith("_F"))) {
			softwareContextOriginal = softwareContext;
			softwareContext = softwareContext.concat(type.substring(
					type.length() - 1, type.length()));

			type = type.substring(0, type.length() - 2);
			logger.debug(
					"Reformatted publication type [{}] without _P or _F suffix",
					type);
		}

		String name = (softwareContext != null && type != null) ? String
				.format("%s %s", softwareContext, type) : null;

		if (name == null)
			logger.warn("Publication (tar file) name [{}] could not be parsed",
					label);

		Element element = new Element();
		element.setType(getPublicationType());
		element.setName(name == null ? label : name);
		element.setFromTimeStamp(new Date());
		element.setCreateTimeStamp(new Date());
		element.setUpdateTimeStamp(new Date());

		Publication configuration = new Publication();
		configuration.setName(element.getName());

		/**
		 * Add publication context by name if not existent
		 */
		if (name != null)
			doPublicationContext(name);

		/**
		 * Organization
		 */
		configuration.setOrganization("se.fk");

		/**
		 * Set version
		 */
		if (version == null) {
			logger.warn("Version not parsed [label: {}]", label);
			configuration.getTag().add("!Version");
			configuration.setVersion("Unknown");
		} else {
			configuration.setVersion(version);
		}

		/**
		 * Prefixed software contexts which is an override of the whole
		 * sub-domain idea (only the last prefix is taken)
		 */
		for (String prefix : prefixes.split("(_|-)")) {
			prefix = prefix.trim();

			if (!prefix.equals("")) {
				Property property = new Property();

				property.setName("software.context.prefix");
				property.setValue(prefix);
				configuration.getProperty().add(property);

				logger.debug(
						"Adding software.context.prefix property {} [label: {}]",
						prefix, label);
			}
		}

		// Publication type
		if (type == null)
			configuration.setType("Unknown");
		else {
			configuration.setType(type);
			doPublicationType(type);
		}

		// Publication extension
		configuration.setExtension("tar");

		// Software context property
		if (softwareContext != null) {
			Property property = new Property();
			property.setName("software.context.ancestor");
			property.setValue(softwareContext);
			configuration.getProperty().add(property);
		}

		// Original software context property
		if (softwareContextOriginal != null) {
			Property property = new Property();
			property.setName("software.context.ancestor.original");
			property.setValue(softwareContextOriginal);
			configuration.getProperty().add(property);
		}

		// Original label
		configuration.setLabel(label);

		element.setConfiguration(configuration);
		return element;
	}

	/**
	 * 
	 * @param name
	 * @return
	 */
	public boolean isSoftwareContext(String name) {
		if (name == null)
			return false;

		Integer count = elementService
				.count(Arrays.asList(new String[] { String
						.format("declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; /pojo:Element[pojo:name eq \"%s\" and empty(pojo:toTimeStamp)][pojo:type/pojo:name = \"{http://www.klistret.com/cmdb/ci/element/context}Software\"]",
								name) }));

		if (count == 1)
			return true;

		if (count > 1)
			logger.error("Software (context) [name: {}] exists {} times", name,
					count);

		return false;
	}

	/**
	 * 
	 * @param name
	 * @return
	 */
	public boolean isOrganizationSoftwareType(String name) {
		if (name == null)
			return false;

		Integer count = elementService
				.count(Arrays.asList(new String[] { String
						.format("declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; /pojo:Element[pojo:name eq \"%s\" and empty(pojo:toTimeStamp)][pojo:type/pojo:name = \"{http://www.klistret.com/cmdb/ci/element/context}OrganizationSoftwareType\"]",
								name) }));

		if (count == 1)
			return true;

		if (count > 1)
			logger.error(
					"Organization software type [name: {}] exists {} times",
					name, count);

		return false;
	}

	/**
	 * 
	 * @param name
	 * @return
	 */
	protected boolean isOrganization(String name) {
		if (name == null)
			return false;

		Integer count = elementService
				.count(Arrays.asList(new String[] { String
						.format("declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; /pojo:Element[pojo:name eq \"%s\" and empty(pojo:toTimeStamp)][pojo:type/pojo:name = \"{http://www.klistret.com/cmdb/ci/element/context}Organization\"]",
								name) }));

		if (count == 1)
			return true;

		if (count > 1)
			logger.error("Organization [name: {}] exists {} times", name, count);

		return false;
	}

	protected void doPublicationType(String name) {
		Integer count = elementService
				.count(Arrays.asList(new String[] { String
						.format("declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; /pojo:Element[pojo:name eq \"%s\" and empty(pojo:toTimeStamp)][pojo:type/pojo:name = \"{http://www.klistret.com/cmdb/ci/element/context}PublicationType\"]",
								name) }));
		if (count == 0) {
			ElementType elementType = elementTypeService
					.get("{http://www.klistret.com/cmdb/ci/element/context}PublicationType");

			Element element = new Element();
			element.setType(elementType);
			element.setName(name);
			element.setFromTimeStamp(new Date());
			element.setToTimeStamp(null);
			element.setCreateTimeStamp(new Date());
			element.setUpdateTimeStamp(new Date());

			PublicationType configuration = new PublicationType();
			configuration.setName(name);

			element.setConfiguration(configuration);

			elementService.create(element);
		}

		if (count > 1)
			logger.error("Publicat Type (context) [name: {}] exists {} times",
					name, count);
	}

	protected void doPublicationContext(String name) {
		Integer count = elementService
				.count(Arrays.asList(new String[] { String
						.format("declare namespace pojo=\"http://www.klistret.com/cmdb/ci/pojo\"; /pojo:Element[pojo:name eq \"%s\" and empty(pojo:toTimeStamp)][pojo:type/pojo:name = \"{http://www.klistret.com/cmdb/ci/element/context}Publication\"]",
								name) }));

		if (count == 0) {
			ElementType elementType = elementTypeService
					.get("{http://www.klistret.com/cmdb/ci/element/context}Publication");

			Element element = new Element();
			element.setType(elementType);
			element.setName(name);
			element.setFromTimeStamp(new Date());
			element.setToTimeStamp(null);
			element.setCreateTimeStamp(new Date());
			element.setUpdateTimeStamp(new Date());

			com.klistret.cmdb.ci.element.context.Publication configuration = new com.klistret.cmdb.ci.element.context.Publication();
			configuration.setName(name);

			element.setConfiguration(configuration);

			elementService.create(element);
		}

		if (count > 1)
			logger.error("Publication (context) [name: {}] exists {} times",
					name, count);
	}
}
